/*
 * File     : PrimitiveReferenceParameter.java
 * Created  : 14 May 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.call;

import static org.objectweb.asm.Opcodes.*;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.Map;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import com.googlecode.dni.internal.DniTypes;
import com.googlecode.dni.internal.MemoryAccess;
import com.googlecode.dni.library.IllegalLibraryException;
import com.googlecode.dni.type.DoubleReference;
import com.googlecode.dni.type.FloatReference;
import com.googlecode.dni.type.IntReference;
import com.googlecode.dni.type.LongReference;

/**
 * Encapsulates a primitive reference parameter.
 *
 * @see IntReference
 * @see LongReference
 * @see FloatReference
 * @see DoubleReference
 *
 * @author Matthew Wilson
 */
final class PrimitiveReferenceParameter extends Parameter
{

    private static final Map< Class< ? >, Class< ? > > REFERENCE_TO_PRIMITIVE =
        new HashMap< Class< ? >, Class< ? > >();

    static
    {
        REFERENCE_TO_PRIMITIVE.put( IntReference.class, int.class );
        REFERENCE_TO_PRIMITIVE.put( LongReference.class, long.class );
        REFERENCE_TO_PRIMITIVE.put( FloatReference.class, float.class );
        REFERENCE_TO_PRIMITIVE.put( DoubleReference.class, double.class );
    }


    private final boolean unsigned;

    /** What size is the native code expecting when deferencing the pointer? */
    private final char effectiveNativeType;


    /**
     * @param type
     *            the parameter type
     * @param nativeTypeAnnotation
     *            one of the NativeXxx annotations, or <code>null</code>
     * @param unsigned
     *            whether unsigned
     */
    PrimitiveReferenceParameter( final Class< ? > type,
                                 final Annotation nativeTypeAnnotation,
                                 final boolean unsigned )
    {
        this.unsigned = unsigned;

        Class< ? > primitiveType = REFERENCE_TO_PRIMITIVE.get( type );
        if ( primitiveType == null )
        {
            throw new IllegalLibraryException( "Unsupported reference type: " + type.getName() );
        }

        if ( nativeTypeAnnotation != null )
        {
            Class< ? extends Annotation > annotationType = nativeTypeAnnotation.annotationType();
            Character nativeType = NativeCallFactory.NATIVE_SIGNATURES.get( annotationType );
            if ( nativeType == null )
            {
                throw new IllegalLibraryException( "Unsupported annotation " + nativeTypeAnnotation.annotationType().getName() + " for: " + type.getName() );
            }
            this.effectiveNativeType = nativeType;

            Class< ? > expectedType = NativeCallFactory.NATIVE_TYPES.get( nativeTypeAnnotation.annotationType() );
            if ( expectedType != primitiveType )
            {
                throw new IllegalLibraryException( "Wrong reference type for " + annotationType + "; got " + type.getName() );
            }
        }
        else
        {
            this.effectiveNativeType = NativeCallFactory.NATIVE_SIGNATURES.get( primitiveType );

            if ( unsigned )
            {
                throw new IllegalLibraryException( "Unnecessary @Unsigned annotation: " + type.getName() );
            }
        }

        setNativeTypeAsPointer();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    int getJavaVarSlots()
    {
        return 1;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    boolean needsScratchMemory()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void writeScratchMemorySize( final MethodVisitor methodVisitor )
    {
        if ( this.effectiveNativeType == 'J' || this.effectiveNativeType == 'D' )
        {
            methodVisitor.visitIntInsn( Opcodes.BIPUSH, 8 );
        }
        else
        {
            methodVisitor.visitInsn( Opcodes.ICONST_4 );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void writePrepareParameter( final MethodVisitor methodVisitor )
    {
        writeGetScratchPointer64( methodVisitor );
        methodVisitor.visitVarInsn( Opcodes.ALOAD, getJavaVarIndex() );

        boolean isLong = false;
        if ( getJavaType() == IntReference.class )
        {
            methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                           DniTypes.INT_REFERENCE_TYPE,
                                           "getValue",
                                           "()I" );
        }
        else if ( getJavaType() == LongReference.class )
        {
            isLong = true;
            methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                           DniTypes.LONG_REFERENCE_TYPE,
                                           "getValue",
                                           "()J" );
        }
        else if ( getJavaType() == FloatReference.class )
        {
            methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                           DniTypes.FLOAT_REFERENCE_TYPE,
                                           "getValue",
                                           "()F" );
        }
        else if ( getJavaType() == DoubleReference.class )
        {
            methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                           DniTypes.DOUBLE_REFERENCE_TYPE,
                                           "getValue",
                                           "()D" );
        }
        else
        {
            throw new AssertionError( getJavaType() );
        }

        switch ( this.effectiveNativeType )
        {
            case 'B':
            case 'C':
            case 'S':
            case 'I':
                if ( isLong )
                {
                    methodVisitor.visitInsn( Opcodes.L2I );
                }
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "putInt",
                                               "(JI)V" );
                break;

            case 'J':
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "putLong",
                                               "(JJ)V" );
                break;

            case 'F':
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "putFloat",
                                               "(JF)V" );
                break;

            case 'D':
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "putDouble",
                                               "(JD)V" );
                break;

            default:
                throw new AssertionError( this.effectiveNativeType );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void writeGetParameter( final MethodVisitor methodVisitor )
    {
        writeGetScratchPointer();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    boolean needsTidying()
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void writeTidyParameter( final MethodVisitor methodVisitor,
                             final boolean isExceptionHandlerBlock )
    {
        if ( isExceptionHandlerBlock )
        {
            // don't update in exception handling
            return;
        }

        methodVisitor.visitVarInsn( Opcodes.ALOAD, getJavaVarIndex() );

        boolean isInt = false;
        writeGetScratchPointer64( methodVisitor );
        switch ( this.effectiveNativeType )
        {
            case 'B':
            case 'C':
            case 'S':
            case 'I':
                isInt = true;
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "getInt",
                                               "(J)I" );
                break;

            case 'J':
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "getLong",
                                               "(J)J" );
                break;

            case 'F':
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "getFloat",
                                               "(J)F" );
                break;

            case 'D':
                methodVisitor.visitMethodInsn( Opcodes.INVOKESTATIC,
                                               MemoryAccess.UNSAFE_ACCESS_TYPE,
                                               "getDouble",
                                               "(J)D" );
                break;

            default:
                throw new AssertionError( this.effectiveNativeType );
        }

        if ( getJavaType() == IntReference.class )
        {
            if ( this.unsigned )
            {
                convertToUnsignedInt( methodVisitor );
            }
            methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                           DniTypes.INT_REFERENCE_TYPE,
                                           "setValue",
                                           "(I)V" );
        }
        else if ( getJavaType() == LongReference.class )
        {
            if ( isInt )
            {
                methodVisitor.visitInsn( I2L );
            }
            if ( this.unsigned )
            {
                convertToUnsignedLong( methodVisitor );
            }
            methodVisitor.visitMethodInsn( INVOKEVIRTUAL,
                                           DniTypes.LONG_REFERENCE_TYPE,
                                           "setValue",
                                           "(J)V" );
        }
        else if ( getJavaType() == FloatReference.class )
        {
            methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                           DniTypes.FLOAT_REFERENCE_TYPE,
                                           "setValue",
                                           "(F)V" );
        }
        else if ( getJavaType() == DoubleReference.class )
        {
            methodVisitor.visitMethodInsn( Opcodes.INVOKEVIRTUAL,
                                           DniTypes.DOUBLE_REFERENCE_TYPE,
                                           "getValue",
                                           "(D)V" );
        }
        else
        {
            throw new AssertionError( getJavaType() );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void writeReturnParameter( final MethodVisitor methodVisitor, final int returnVarIndex )
    {
        throw new UnsupportedOperationException();
    }

    private void convertToUnsignedInt( final MethodVisitor methodVisitor )
    {
        switch ( this.effectiveNativeType )
        {
            case 'B':
                methodVisitor.visitIntInsn( SIPUSH, 0xff );
                methodVisitor.visitInsn( IAND );
                break;

            case 'C':
            case 'S':
                methodVisitor.visitLdcInsn( 0xffff );
                methodVisitor.visitInsn( IAND );
                break;

            case 'I':
                break;

            default:
                throw new AssertionError( this.effectiveNativeType );
        }
    }

    private void convertToUnsignedLong( final MethodVisitor methodVisitor )
    {
        switch ( this.effectiveNativeType )
        {
            case 'B':
                methodVisitor.visitLdcInsn( 0xffL );
                methodVisitor.visitInsn( LAND );
                break;

            case 'C':
            case 'S':
                methodVisitor.visitLdcInsn( 0xffffffffL );
                methodVisitor.visitInsn( LAND );
                break;

            case 'I':
                break;

            default:
                throw new AssertionError( this.effectiveNativeType );
        }
    }

}
